package com.github.tommas.admintpl.service;

import com.github.tommas.admintpl.bean.filestorage.FileStorage;
import com.github.tommas.admintpl.bean.model.FileInfoModel;
import com.github.tommas.admintpl.bean.vo.File;
import com.github.tommas.admintpl.dao.FileStorageMapper;
import com.github.tommas.admintpl.dao.UserMapper;
import com.github.tommas.admintpl.mybatis.Context;
import com.github.tommas.admintpl.service.exception.FileStorageException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

@Service
public class FileStorageService {
    private static final Logger LOGGER = LoggerFactory.getLogger(FileStorageService.class);

    private static final int MAX_UPLOAD_THREAD_SIZE = 4;

    @Autowired
    private FileStorage fileStorage;
    @Autowired
    private FileStorageMapper fileStorageMapper;
    @Autowired
    private UserMapper userMapper;

    public FileInfoModel store(InputStream inputStream, String filename, Integer size, Integer targetId, String businessType, Integer deptId) {
        try {
            String storageId = fileStorage.store(inputStream, filename);

            FileInfoModel m = new FileInfoModel();
            m.setTargetId(targetId);
            m.setBusinessType(businessType);
            m.setStorageId(storageId);
            m.setName(filename);
            m.setSize(size);
            m.setDeptId(deptId);

            createFileInfoList(Collections.singletonList(m));

            return m;
        } catch (Throwable throwable) {
            LOGGER.error(String.format("Failed to store file: %s", filename), throwable);
            throw new FileStorageException(Collections.singletonList(filename));
        }
    }

    /**
     * @param timeout max upload time in seconds
     * @param allowPartialStore allow partial files to be stored or throw exception
     */
    public List<FileInfoModel> storeInBatch(File[] files, Integer targetId, String businessType, Integer deptId, long timeout, boolean allowPartialStore) {
        CountDownLatch latch = new CountDownLatch(files.length);
        FileInfoModel[] models = new FileInfoModel[files.length];
        Exception[] exceptions = new Exception[files.length];
        int cnt = 0;
        ExecutorService executorService = Executors.newFixedThreadPool(MAX_UPLOAD_THREAD_SIZE);
        for (File f : files) {
            final int i = cnt++;
            executorService.submit(() -> {
                String filename = f.getName();
                try {
                    FileInfoModel m = new FileInfoModel();
                    m.setTargetId(targetId);
                    m.setBusinessType(businessType);
                    m.setName(filename);
                    m.setSize(f.getSize());
                    m.setDeptId(deptId);

                    String storageId = fileStorage.store(f.getInputStream(), filename);
                    m.setStorageId(storageId);

                    models[i] = m;
                } catch (Exception e) {
                    exceptions[i] = e;
                } finally {
                    latch.countDown();
                }
            });
        }

        try {
            if (timeout > 0) {
                if (!latch.await(timeout, TimeUnit.SECONDS)) {
                    LOGGER.error("Max upload time: {} exceeded", timeout);
                }
            } else {
                latch.await();
            }
        } catch (InterruptedException e) {
            LOGGER.error("Waiting upload thread interrupted");
        }

        List<String> failed = new ArrayList<>();
        List<FileInfoModel> l = new ArrayList<>(files.length);
        for (int i = 0; i < files.length; i++) {
            if (exceptions[i] != null) {
                String filename = models[i].getName();
                failed.add(filename);
                LOGGER.error(String.format("Failed to store file: %s", filename), exceptions[i]);
            } else {
                l.add(models[i]);
            }
        }

        if (!failed.isEmpty()) {
            if (failed.size() == files.length || !allowPartialStore) {
                throw new FileStorageException(failed);
            }
        }

        return createFileInfoList(l);
    }

    @Transactional
    private List<FileInfoModel> createFileInfoList(List<FileInfoModel> models) {
        fileStorageMapper.insertFileInfos(Context.insertContext(new FileInfoModel()), models);
        return models;
    }

    public File getFile(Integer id, Integer targetId, String businessType) {
        FileInfoModel m = fileStorageMapper.findFileInfo(id, targetId, businessType);
        if (m == null) {
            return null;
        }

        File file = new File();
        file.setName(m.getName());
        file.setSize(m.getSize());
        file.setInputStream(fileStorage.get(m.getStorageId()));

        return file;
    }

    public List<FileInfoModel> getFileInfoByTarget(Integer targetId, String businessType) {
        return fileStorageMapper.findFileInfoByTarget(targetId, businessType);
    }

    public int setFileTarget(Integer targetId, String businessType, List<Integer> fileIds) {
        return fileStorageMapper.updateTargetFileInfo(targetId, businessType, fileIds);
    }
}
